Scenarios

Important

Please note that this part of the documentation is not yet finished. It will still be revised and updated.

A (test) scenario describes the environment that a test needs to be able to be carried out (in contrast to the Setups that describes what you have). A scenario allows to define a test environment first, after the individual test cases will be implemented.

Create a scenario

Every scenario class have to be located in a file that fulfils the naming scenario_*.py. To keep it clear, it is often useful to create a own directory for the scenario which has the same name like the scenario python file. So you can define your related objects, you only use for your single scenario inside this directory. However Balder will load the python file (with naming scenario_*.py) in its collecting process and searches for classes that are a subclass of Scenario and starts with the name Scenario.

# file `scenario_simple_send_msg.py`

import balder

class ScenarioSimpleSendMsg(balder.Scenario):
        pass

Get it back in your mind, a scenario defines the things your test needs. The most obvious is that you want to test something, usually a device or an object. For this Balder provides Device classes.

Add one or more devices

If you need more than one Device classes, you should also define their relationship. This can be done by connecting them. Let’s assume, we want to test the connection between two devices. The devices should send a msg between each other. For this example, it doesn’t matter over which connection these two devices are connected, because we can send a message over an EthernetConnection as well as a morse signal. So we define our scenario connection (remember, defines what we need) with the highest universal Connection. We can connect two devices over the @balder.connect() decorator.

Let’s expand our example with the two devices and there connection:

# file `scenario_simple_send_msg/scenario_simple_send_msg.py`

import balder

class ScenarioSimpleSendMsg(balder.Scenario):

        class SendDevice(balder.Device):
                pass

        @balder.connect(SendDevice, over_connection=balder.Connection)
        class RecvDevice(balder.Device):
                pass

Let’s just capture the whole thing again. Device classes are inner classes that defines the devices we need for our test scenario. They always have to be subclasses of Device. These devices can be in a relationship with each other. For this relationship, we use the @balder.connect(..) decorator. You can read more about devices at Devices.

In this example, we have used the universal Connection object, but there are a lot of other connections too. You can also define some by your own.

Note

These connection objects are already in a relationship before you use them. They are included in a global connection-tree. This tree defines a hierarchical structure of the connections (for example, that Ethernet can be transmitted over a CoaxialCableConnection or a OpticalFiberConnection.

It is also possible to expand this tree by your own or if necessary to use a complete custom tree.

You can read more about this here.

In addition to define single connections, you can also select a part of the global connection tree or combine some connections with an OR or an AND relationship. So for example you could connect our devices and allow an Ethernet as well as a Serial connection, by defining @balder.connect(SendDevice, over_connection=Connection.based_on(MyEthernet, MySerial)). Of course you could also define, that you need both, the Serial and the Ethernet connection. This can be done by using tuples: @balder.connect(SendDevice, over_connection=Connection.based_on((MyEthernet, MySerial)))

In our example we only define that we want a universal Connection between our devices SendDevice and RecvDevice. With this the connection type doesn’t matter and every connection works here.

Add new device features

Now we have two devices, but they can’t do anything yet. We can add functionality to them by creating or using so called Feature classes. We want to define some by ourselves. For this we add a new file features.py inside our scenario directory, we’ve created before. For this example we need one feature that can send messages and one that can receive the sent messages. First let us define these new features without an implementation:

# file `scenario_simple_send_msg/features.py`

import balder

class SendMessageFeature(balder.Feature):
        pass

class RecvMessageFeature(balder.Feature):
        pass

You can assign a feature to a scenario-device in a way that this scenario device now needs this feature for an execution by instantiating it as class attribute inside the device:

# file `scenario_simple_send_msg/scenario_simple_send_msg.py`

import balder
from .features import SendMessageFeature, RecvMessageFeature

class ScenarioSimpleSendMsg(balder.Scenario):

        class SendDevice(balder.Device):
                send = SendMessageFeature()

        @balder.connect(SendDevice, over_connection=balder.Connection)
        class RecvDevice(balder.Device):
                recv = RecvMessageFeature()

As you can see above, we have to instantiate our new Feature classes as class attribute of the device classes. With this we want to define that they implement it.

In this example we define that we need a SendDevice which has a SendMessageFeature and a RecvDevice which has the RecvMessageFeature. Both have to be connected over a universal Connection. These are the things, we need in a setup later, to allow the execution of this scenario. Otherwise the variation between the not-working setup and this scenario is not applicable. Balder uses this information to check if a variation (matching between a setup and a scenario) is possible or not.

Add real functionality

Up to now we have defined some Features, but they still have no real implementation. So we can’t really do something with them.

Now we want to update our features to add some methods. We expand our features.py file a little bit:

# file `scenario_simple_send_msg/features.py`

import balder

class SendMessageFeature(balder.Feature):

    @property
    def address(self):
        raise NotImplementedError("has to be implemented in subclass")

    def send_bytes_to(self, other, the_bytes):
        """sends the bytes to the object"""
        raise NotImplementedError("has to be implemented in subclass")

class RecvMessageFeature(balder.Feature):

    @property
    def address(self):
        raise NotImplementedError("has to be implemented in subclass")

    def listen_for_incoming_msgs(timeout):
        """returns list with tuples (sender_object, the_bytes)"""
        raise NotImplementedError("has to be implemented in subclass")

With that, we added two abstract methods without an implementation yet. We are going to implemented them in the Feature subclass of our Setups later.

Note

In some cases it can be useful to provide a implementation in the scenario-feature implementation too. You can find more details about that in the Features section.

Use the features and write tests

Now we can write our first test method. We want to send a Hello-World message and want to make sure that it was received successfully. It is important that the name of a test method always starts with test_*(), otherwise Balder will not collect it as a testcase.

# file `scenario_simple_send_msg/scenario_simple_send_msg.py`

import balder
from .features import SendMessageFeature, RecvMessageFeature

class ScenarioSimpleSendMsg(balder.Scenario):

    class SendDevice(balder.Device):
        send = SendMessageFeature()

    @balder.connect(SendDevice, over_connection=balder.Connection)
    class RecvDevice(balder.Device):
        recv = RecvMessageFeature()

    def test_simple(self):
        send_msg = b"Hello World!"
        self.SendDevice.send.send_bytes_to(self.RecvDevice.recv.address, send_msg)
        recv_list = self.RecvDevice.listen_for_incoming_msgs(timeout=1)
        assert (self.SendDevice.send.address, send_msg) in recv_list, "can not find the message in received message list"

It is very easy to access a device inside a test method. With self.SendDevice or self.RecvDevice we can access our created devices and over their class attributes we can access the Features objects too. This allows us to execute our newly created properties and methods.

Scenario inheritance

Warning

This section is still under development.