-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added some skill testing utilities in pytlas.testing
- Loading branch information
Showing
4 changed files
with
196 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |