# Appdaemon Test Framework
[![Travis](https://img.shields.io/travis/FlorianKempenich/Appdaemon-Test-Framework.svg)](https://travis-ci.org/FlorianKempenich/Appdaemon-Test-Framework) [![PyPI](https://img.shields.io/pypi/v/appdaemontestframework.svg)](https://pypi.org/project/appdaemontestframework/)
Clean, human-readable tests for your Appdaemon automations.
* Totally transparent, No code modification is needed.
* Mock the state of your home: `given_that.state_of('sensor.temperature').is_set_to('24.9')`
* Seamless assertions: `assert_that('light.bathroom').was.turned_on()`
* Simulate time: `time_travel.fast_forward(2).minutes()`
##### How does it look?
```python
def test_during_night_light_turn_on(given_that, living_room, assert_that):
given_that.state_of('sensor.living_room_illumination').is_set_to(200) # 200lm == night
living_room._new_motion(None, None, None)
assert_that('light.living_room').was.turned_on()
def test_click_light_turn_on_for_5_minutes(given_that, living_room, assert_that):
living_room._new_button_click(None, None, None)
assert_that('light.bathroom').was.turned_on()
# At T=4min light should not yet have been turned off
time_travel.fast_forward(4).minutes()
assert_that('light.bathroom').was_not.turned_off()
# At T=5min light should have been turned off
time_travel.fast_forward(1).minutes()
time_travel.assert_current_time(5).minutes()
assert_that('light.bathroom').was.turned_off()
```
---
## Table of Contents
- [5-Minutes Quick Start Guide](#5-minutes-quick-start-guide)
* [Initial Setup](#initial-setup)
* [Write you first unit test](#write-you-first-unit-test)
* [Result](#result)
- [General Test Flow and Available helpers](#general-test-flow-and-available-helpers)
* [0. Initialize the automation: `@automation_fixture`](#0-initialize-the-automation-automation_fixture)
* [1. Set the stage to prepare for the test: `given_that`](#1-set-the-stage-to-prepare-for-the-test-given_that)
* [2. Trigger action on your automation](#2-trigger-action-on-your-automation)
* [3. Assert on your way out: `assert_that`](#3-assert-on-your-way-out-assert_that)
* [Bonus — Assert callbacks were registered during `initialize()`](#bonus--assert-callbacks-were-registered-during-initialize)
* [Bonus — Travel in Time: `time_travel`](#bonus--travel-in-time-time_travel)
- [Examples](#examples)
- [Under The Hood](#under-the-hood)
- [Advanced Usage](#advanced-usage)
- [Contributing](#contributing)
- [Author Information](#author-information)
---
## 5-Minutes Quick Start Guide
### Initial Setup
1. Install **pytest**: `pip install pytest`
1. Install the **framework**: `pip install appdaemontestframework`
1. Copy [**`conftest.py`**](https://github.com/FlorianKempenich/Appdaemon-Test-Framework/blob/new-features/doc/full_example/conftest.py) at the **root** of your project
### Write you first unit test
Let's test an Appdaemon automation we created, which, say, handles automatic lighting in the Living Room: `class LivingRoom`
<!-- We called the class `LivingRoom`. Since it's an Appdaemon automation, its lifecycle is handled -->
1. **Initialize** the Automation Under Test with the `@automation_fixture` decorator:
```python
@automation_fixture(LivingRoom)
def living_room():
pass
```
1. **Write your first test:**
##### Our first unit test
```python
def test_during_night_light_turn_on(given_that, living_room, assert_that):
given_that.state_of('sensor.living_room_illumination').is_set_to(200) # 200lm == night
living_room._new_motion(None, None, None)
assert_that('light.living_room').was.turned_on()
```
> ##### Note
> 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**:
> * `living_room`
> * `given_that`
> * `assert_that`
> * `time_travel`
### Result
```python
# Important:
# For this example to work, do not forget to copy the `conftest.py` file.
@automation_fixture(LivingRoom)
def living_room():
pass
def test_during_night_light_turn_on(given_that, living_room, assert_that):
given_that.state_of('sensor.living_room_illumination').is_set_to(200) # 200lm == night
living_room._new_motion(None, None, None)
assert_that('light.living_room').was.turned_on()
def test_during_day_light_DOES_NOT_turn_on(given_that, living_room, assert_that):
given_that.state_of('sensor.living_room_illumination').is_set_to(1000) # 1000lm == sunlight
living_room._new_motion(None, None, None)
assert_that('light.living_room').was_not.turned_on()
```
---
## General Test Flow and Available helpers
### 0. Initialize the automation: `@automation_fixture`
```python
# Command
@automation_fixture(AUTOMATION_CLASS)
def FIXTURE_NAME():
pass
# Example
@automation_fixture(LivingRoom)
def living_room():
pass
```
The automation given to the fixture will be:
* **Created**
_Using the required mocks provided by the framework_
* **Initialized**
_By calling the `initialize()` function_
* **Made available as an injectable fixture**
_Just like a regular `@pytest.fixture`_
### 1. Set the stage to prepare for the test: `given_that`
* #### Simulate args passed via `apps.yaml` config
See: [Appdaemon - Passing arguments to Apps](http://appdaemon.readthedocs.io/en/latest/APPGUIDE.html#passing-arguments-to-apps)
```python
# Command
given_that.passed_arg(ARG_KEY).is_set_to(ARG_VAL)
# Example
given_that.passed_arg('color').is_set_to('blue')
```
* #### State
```python
# Command
given_that.state_of(ENTITY_ID).is_set_to(STATE_TO_SET)
given_that.state_of(ENTITY_ID).is_set_to(STATE_TO_SET, ATTRIBUTES_AS_DICT)
# Example
given_that.state_of('media_player.speaker').is_set_to('playing')
given_that.state_of('light.kitchen').is_set_to('on', {'brightness': 50,
'color_temp': 450})
```
* #### Time
```python
# Command
given_that.time_is(TIME_AS_DATETIME)
# Example
given_that.time_is(time(hour=20))
```
* #### Extra
```python
# Clear all calls recorded on the mocks
given_that.mock_functions_are_cleared()
# To also clear all mocked state, use the option: 'clear_mock_states'
given_that.mock_functions_are_cleared(clear_mock_states=True)
# To also clear all mocked passed args, use the option: 'clear_mock_passed_args'
given_that.mock_functions_are_cleared(clear_mock_passed_args=True)
```
### 2. Trigger action on your automation
The way Automations work in Appdaemon is:
* First you **register callback methods** during the `initialize()` phase
* At some point **Appdaemon will trigger these callbacks**
* Your Automation **reacts to the call on the callback**
To **trigger actions** on your automation, simply **call one of the registered callbacks**.
> #### Note
> It is best-practice to have an initial test that will test the callbacks
> are _actually_ registered as expected during the `initialize()` phase.
> See: [Bonus - Assert callbacks were registered during `initialize()`](#bonus--assert-callbacks-were-registered-during-initialize)
#### Example
##### `LivingRoomTest.py`
```python
def test_during_night_light_turn_on(given_that, living_room, assert_that):
...
living_room._new_motion(None, None, None)
...
```
##### With `LivingRoom.py`
```python
class LivingRoom(hass.Hass):
def initialize(self):
...
self.listen_event(
self._new_motion,
'motion',
entity_id='binary_sensor.bathroom_motion')
...
def _new_motion(self, event_name, data, kwargs):
< Handle motion here >
```
### 3. Assert on your way out: `assert_that`
* #### Ent