HelloThisIsFlo/Appdaemon-Test-Framework

time_travel does not work (possibly incorrect use)

danobot opened this issue ยท 4 comments

Hi,
I'm a developer and am having some problems getting the time travel to work,

import pytest

from apps.demo_class import MotionLight

# Important:
# For this example to work, do not forget to copy the `conftest.py` file.
# See README.md for more info
TEST_LIGHT = 'light.test_light';
TEST_SENSOR = 'binary_sensor.test_sensor';
DELAY = 120;
@pytest.fixture
def motion_light(given_that):
    motion_light = MotionLight(None, None, None, None, None, None, None, None)
    given_that.time_is(0)
    motion_light.initialize()
    given_that.mock_functions_are_cleared()
    return motion_light


def test_demo(given_that, motion_light, assert_that, time_travel):
    given_that.state_of('light.test_light').is_set_to('on') 
    time_travel.assert_current_time(0).seconds()
    motion_light.motion(DELAY)
    time_travel.fast_forward(DELAY).seconds()
    time_travel.assert_current_time(DELAY).seconds()
    assert_that('light.test_light').was.turned_off()

Here is the demo class i am trying to test:

import appdaemon.plugins.hass.hassapi as hass

class MotionLight(hass.Hass):
    def initialize(self):
        self.timer = None

    def motion(self, delay):
        """
            Sensor callback: Called when the supplied sensor/s change state.
        """

        self.start_timer(delay);
    
    def start_timer(self, delay):
        if self.timer:
            self.cancel_timer(self.timer) # cancel previous timer
            self.timer = self.run_in(self.light_off, delay)

    def light_off(self):
        self.log("fds");
        self.turn_off('light.test_light')

For some reason, even though run_in time and time_travel time match, the turn off service call is not found.

=================================== FAILURES ===================================
__________________________________ test_demo ___________________________________

given_that = <appdaemontestframework.given_that.GivenThatWrapper object at 0x7fde397b00f0>
motion_light = <apps.demo_class.MotionLight object at 0x7fde39755a90>
assert_that = <appdaemontestframework.assert_that.AssertThatWrapper object at 0x7fde39755ac8>
time_travel = <appdaemontestframework.time_travel.TimeTravelWrapper object at 0x7fde39755b38>

    def test_demo(given_that, motion_light, assert_that, time_travel):
        given_that.state_of('light.test_light').is_set_to('on')
        time_travel.assert_current_time(0).seconds()
        motion_light.motion(DELAY)
        time_travel.fast_forward(DELAY).seconds()
        time_travel.assert_current_time(DELAY).seconds()
>       assert_that('light.test_light').was.turned_off()

tests/test_motion_lights.py:26:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <appdaemontestframework.assert_that.WasWrapper object at 0x7fde39755ba8>

    def turned_off(self):
        """ Assert that a given entity_id has been turned off """
        entity_id = self.thing_to_check

        service_not_called = _capture_assert_failure_exception(
            lambda: self.hass_functions['call_service'].assert_any_call(
                ServiceOnAnyDomain('turn_off'),
                entity_id=entity_id))

        turn_off_helper_not_called = _capture_assert_failure_exception(
            lambda: self.hass_functions['turn_off'].assert_any_call(
                entity_id))

        if service_not_called and turn_off_helper_not_called:
            raise EitherOrAssertionError(
>               service_not_called, turn_off_helper_not_called)
E           appdaemontestframework.assert_that.EitherOrAssertionError:
E
E           At least ONE of the following exceptions should not have been raised
E
E           The problem is EITHER:
E           call_service('ANY_DOMAIN/turn_off', entity_id='light.test_light') call not found
E
E           OR
E           turn_off('light.test_light') call not found

/usr/local/lib/python3.6/site-packages/appdaemontestframework/assert_that.py:106: EitherOrAssertionError
=========================== 1 failed in 0.08 seconds ===========================

Sorry. just got a demo test to work and found a defect in my appdaemon script. Automated testing is already helping me out a lot :) Awesome work on this framework man. Closing!

I'm so happy to see this project is helping other people. It's really just a thing that made my life easier and thought I'd share . . . . but didn't expect anyone to actually use it ๐Ÿ™ƒ
So yeah it's really cool! Thank you for the encouragement ๐Ÿ™‚

On a side note:
cancel_timer isn't supported. As a matter of fact, time_travel only works with the run_in function.
That might change in the future . . . or might not. No commitement so far, I'll see if I need it enough to spend the time on it ๐Ÿ˜‰

Created parameterised test cases with it. Totalling 81 test cases so far. All pass <3
I'm migrating to a state machine based implementation for my app. These automated tests will help me ensure the appdaemon app continues to do what it does (despite a very different implementation inside).
What else is this framework missing that you're aware of?

A state machine, nice! Were you inspired by my smart bathroom? ^_^

Regarding your question, basically, any function not patched in init_framework.py - L20 is not supported.

Adding a function is a pretty straightforward process, the function would then be available in the hass_functions fixture. You can add a pull request any-time. But leveraging that function in a way that's seamless in one of the higher level fixtures like assert_that, given_that and time_travel requires a bit more thinking. You are welcome as well to submit a pull request for modifying these, but then we'd have a conversation to see if that makes sense, and if not, find a way to tweak it in a way that does ;)

Oh and just thought of a tip for your state machine. As you can see in my example, implementing a "regular" / "in-memory" state machine works well, but testing can get a bit cumbersome (to get in the given state at the beginning of each test). On another project, I found a solution that makes everything so much more seamless: Externalize the current state in an input_select stored in HomeAssistant. Basically taking the functional implementation of a state-machine and using Home Assistant as a database. Not only, does it makes testing incredibly easier, it also provides a live view of the inner state of each room, as well as their history, etc. All for free. It's really cool :)
But then again, it's just a tip ;)