Skip to content

Commit

Permalink
Added some skill testing utilities in pytlas.testing
Browse files Browse the repository at this point in the history
  • Loading branch information
YuukanOO committed Mar 13, 2019
1 parent 7807119 commit c8ffcd9
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 1 deletion.
32 changes: 32 additions & 0 deletions example/skills/lights/test_lights.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from sure import expect
from pytlas.testing import create_skill_agent
import os

agent = create_skill_agent(os.path.dirname(__file__))

class TestLights:

def setup(self):
agent.model.reset()

def test_it_should_turn_lights_on(self):
agent.parse('Turn the lights on in kitchen please')

call = agent.model.on_answer.get_call(0)

expect(call.text).to.equal('Turning lights on in kitchen')

def test_it_should_turn_lights_on_and_ask_for_rooms(self):
agent.parse('Turn the lights on')

call = agent.model.on_ask.get_call()

expect(call.slot).to.equal('room')
expect(call.text).to.be.within(['For which rooms?', 'Which rooms Sir?', 'Please specify some rooms'])
expect(call.choices).to.be.none

agent.parse('In the kitchen please')

call = agent.model.on_answer.get_call()

expect(call.text).to.equal('Turning lights on in kitchen')
2 changes: 1 addition & 1 deletion pytlas/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def __init__(self, interpreter, model=None, handlers=None, transitions_graph_pat
self._on_thinking = None
self._on_context = None

self._handlers = handlers or skill_handlers
self._handlers = handlers if handlers != None else skill_handlers
self._translations = get_translations(self._interpreter.lang)

self._intents_queue = []
Expand Down
125 changes: 125 additions & 0 deletions pytlas/testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import os, sys
from unittest.mock import MagicMock
from pytlas.importers import import_or_reload
from pytlas.agent import Agent
from pytlas.skill import handlers
from pytlas.utils import get_root_package_name
from pytlas.interpreters.snips import SnipsInterpreter

class AttrDict(dict):
"""Simple object to access dict keys like attributes.
"""

def __getattr__(self, name):
try:
return self[name]
except KeyError:
raise AttributeError

class ModelMock(MagicMock):
"""Represents a particular mocking object used to make assertions easier on the
agent model.
"""

def has_arguments_mapping(self, mapping):
"""Adds arguments names to this instance to be able to test against them. Maybe
there is a better way to handle it, I don't know yet.
Args:
mapping (list of str): List of keys in the order of arguments
"""

self._args_map = mapping
return self

def get_call(self, number=0):
"""Retrieve call args for the given call number. With this tiny method, you can
call a mock multiple times and assert against a specific one.
It will returns an AttrDict which contains each argument name and their respective value
so order is not an issue.
Args:
number (int): Call order
Returns:
AttrDict: AttrDict with argument names as keys
"""

try:
c = self.call_args_list[number][0]
except IndexError:
raise AssertionError('Mock has been called less than %d times!' % number)

r = AttrDict()

for (i, arg) in enumerate(c):
try:
r[self._args_map[i]] = arg
except IndexError:
pass

for (k, v) in self.call_args_list[number][1].items():
r[k] = v

return r

class AgentModelMock:
"""Represents an agent model targeted at testing skills easily.
"""

def __init__(self):
self.on_answer = ModelMock().has_arguments_mapping(['text', 'cards'])
self.on_ask = ModelMock().has_arguments_mapping(['slot', 'text', 'choices'])
self.on_done = ModelMock().has_arguments_mapping(['require_input'])
self.on_context = ModelMock().has_arguments_mapping(['context_name'])
self.on_thinking = ModelMock().has_arguments_mapping([])

def reset(self):
"""Resets all magic mocks calls. Basically, you should call it in your test
setup method to make sure each instance starts from a fresh state.
"""

self.on_answer.reset_mock()
self.on_ask.reset_mock()
self.on_done.reset_mock()
self.on_context.reset_mock()
self.on_thinking.reset_mock()

def create_skill_agent(skill_folder, lang='en', additional_skills=[]):
"""Create an agent specifically targeted at the specified skill folder. It makes
it easy to write skill tests using a specific mock object as the Agent model.
It will spawn a SnipsInterpreter and fit data only for the skill being tested.
Args:
skill_folder (str): Absolute path of the skill folder to be tested
lang (str): Optional language used by the interpreter
additional_skills (list of str): Additional skills to be loaded and interpreted
Returns:
Agent: Agent with a specific mock model to make assertions simplier
"""

import_path = os.path.dirname(skill_folder)
skill_name = os.path.basename(skill_folder)

# Start by importing the skill
if import_path not in sys.path:
sys.path.append(import_path)

import_or_reload(skill_name)

# And instantiate an interpreter
additional_skills.append(skill_name)

interpreter = SnipsInterpreter(lang)
interpreter.fit_from_skill_data(additional_skills)

# Filter handlers for targeted skill only
valid_handlers = { k: v for (k, v) in handlers.items() if get_root_package_name(v.__module__) in additional_skills }

return Agent(interpreter, model=AgentModelMock(), handlers=valid_handlers)
38 changes: 38 additions & 0 deletions tests/test_testing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from sure import expect
from pytlas.testing import AttrDict, ModelMock

class TestAttrDict:

def test_it_should_expose_keys_as_attributes(self):
d = AttrDict({
'one': 1,
'two': 2,
})

expect(d.one).to.equal(1)
expect(d.two).to.equal(2)
expect(lambda: d.three).to.throw(AttributeError)

class TestModelMock:

def test_it_should_take_argument_names_into_account(self):
m = ModelMock().has_arguments_mapping(['one', 'two'])

m(1, 2, three=3)
m(4, 5, three=6)

call = m.get_call()

expect(call).to.be.a(AttrDict)
expect(call.one).to.equal(1)
expect(call.two).to.equal(2)
expect(call.three).to.equal(3)

call = m.get_call(1)

expect(call).to.be.a(AttrDict)
expect(call.one).to.equal(4)
expect(call.two).to.equal(5)
expect(call.three).to.equal(6)

expect(lambda: m.get_call(2)).to.throw(AssertionError)

0 comments on commit c8ffcd9

Please sign in to comment.