In this guide we describe some of the tools the framework offers for testing skills.

The BaseSkillTestCase class

The framework offers a BaseSkillTestCase class which you can subclass and write your test cases with.

Let us assume you want to test the my_behaviour behaviour of a CustomSkill skill you have developed.

You can create a TestMyBehaviour class which inherits BaseSkillTestCase as below:

from aea.test_tools.test_skill import BaseSkillTestCase

class TestMyBehaviour(BaseSkillTestCase):
    """Test my_behaviours of the custom skill."""

    path_to_skill = Path("path_to_this_skill")

Specifying Skill Path

You must then specify the path to your skill directory via path_to_skill to allow the skill to be loaded and tested. This must be the directory in which skill.yaml of your skill resides.

Setting up Each Test

You can add a setup() class method to set the environment up for each of your tests. This code will be executed before every test method. If you do include this method, you must call the setup() method of the BaseSkillTestCase class via super().setup().

@classmethod
def setup(cls):
    """Setup the test class."""
    super().setup()
    cls.my_behaviour = cast(
        MyBehaviour, cls._skill.skill_context.behaviours.my_behaviour
    )

In the above, we make the my_behaviour behaviour object accessible for every test.

Skill and Skill Context

The skill object itself is exposed via a property. So you can access the skill object by self.skill and by extension all of its attributes. This crucially includes the complete skill_context. This means that for example, all of the components of the skill (e.g. behaviours, handlers, models) can be accessed via the skill context.

In the above code snippet, my_behavior is accessed and exposed as a class attribute. Note accessing the skill context is slightly different in the above because it is a class method. If this was a test method, you could access the behaviour via self.skill.skill_context.behaviours.my_behaviour.

Dummy Agent Context

The loaded skill is also fed a dummy agent_context complete with an identity, outbox, decision_maker_queue and so on, to allow the skill to be properly loaded and have access to everything it requires to function. The agent_context object fed to the skill is shown below:

_multiplexer = AsyncMultiplexer()
_multiplexer._out_queue = (asyncio.Queue())

agent_context = AgentContext(
    identity=Identity("test_agent_name", "test_agent_address"),
    connection_status=_multiplexer.connection_status,
    outbox=OutBox(cast(Multiplexer, cls._multiplexer)),
    decision_maker_message_queue=Queue(),
    decision_maker_handler_context=SimpleNamespace(),
    task_manager=TaskManager(),
    default_connection=None,
    default_routing={},
    search_service_address="dummy_search_service_address",
    decision_maker_address="dummy_decision_maker_address",
)

Some Useful Skill Attributes

Some of the useful objects you can access in your test class for the loaded skill are below:

  • self.skill.skill_context.agent_address: this is the agent identity the skill uses and is set to "test_agent_address".
  • self.skill.skill_context.search_service_address: this is the address of the search service and is set to "dummy_search_service_address".
  • self.skill.skill_context.skill_id: this is the id of the skill.
  • self.skill.skill_context.decision_maker_address: this is the address of the decision maker and is set to "dummy_decision_maker_address".

Some Useful BaseSkillTestCase Methods

There are a number of methods that BaseSkillTestCase offers to make testing skills easier. Some of these are mentioned below. For the rest, consult the API for BaseSkillTestCase:

  • self.get_quantity_in_outbox(): gives you the number of messages which are in the outbox. After running a part of the skill which is expected to send messages, you can use this method to assert the correct number of messages are indeed sent.
  • self.get_message_from_outbox(): gives you the last message in the outbox. Together with the above, you can use this method to grab the last message sent by the skill code you tested and check this is indeed the expected message.
  • self.message_has_attributes(actual_message: Message, message_type: Type[Message], **kwargs,): you can use this method in tandem with the above method to check that a message has the attributes you expect it to have. You have to supply it with the actual message (e.g. using self.get_message_from_outbox()), specify its expected type (e.g. FipaMessage), and any other attribute you expect the message to have (e.g. message_id is 1) may be provided via keyword arguments.
  • self.build_incoming_message: this is an especially useful method to test handlers. Since handlers handle incoming messages, you can create an incoming message using this method to feed it to the handler and test its execution.

Checking Logger Output

You can check the output of your skill's logger by mocking it using unittest.mock before executing a part of your skill as such:

with mock.patch.object(self.my_behaviour.context.logger, "log") as mock_logger:
    self.my_behaviour.act()

mock_logger.assert_any_call(logging.INFO, "some_logger_message")

In the above, we mock the logger before running my_behaviour's act() method and check that the string "some_logger_message" is indeed passed to the logger.

Next steps

You can consult the fetchai/generic_buyer and fetchai/generic_seller skills and their associated tests here to study how BaseSkillTestCase can help you in testing your skills.

You can also refer to the API to study the different methods BaseSkillTestCase makes available to make testing your skills easier.