Smart House Tests

In order to show the full example of PatrIoT framework in use, this section will provide a detailed explanation of example source code with testbed as well as testing scenarios. The source codes can be found in the example directory and are composed of the following modules:

  • smart-home-integration-test the project containing tests written with PatrIoT framework.

  • virtual-smart-home a sample implementation of a smart home using the PatrIoT capabilities of data generation.

  • smart-gateway a sample gateway which conducts operations over virtual-smart-home

Prerequisites

This is the list of installed software you’ll need to run this example of integration tests with PatrIoT Framework.

  • Java in version 8 or higher

  • Maven

  • Docker

  • Docker compose

Environment setup

In order to run these tests, you need to have the tested software prepared and ready. To achieve this, you need to produce docker images which are able to run the tested modules which are produced by the following procedure:

  1. Change the directory to the selected component.

  2. Run mvn clean package to produce a compiled java program.

  3. Build the docker image by running docker build . --tag myapp/${COMPONENT}

  4. Do these steps for virtual-smart-home and smart-gateway (e.g. COMPONENT=virtual-smart-home)

Once the components are built and docker images are produced pull the router image by executing following command:

$ docker pull patriotframework/patriot-router:latest

Then the test project can be executed, as a first step it is needed to change directory to the smart-home-integration-test project directory. After that, it is necessary to start up the monitoring environment:

$ docker-compose up -d

The command will start the monitoring environment with output similar to this:

Creating network "smarthousetests_default" with the default driver
Creating smarthousetests_mongodb_1       ... done
Creating smarthousetests_elasticsearch_1 ... done
Creating smarthousetests_kibana_1        ... done
Creating smarthousetests_graylog_1       ... done

Please note the network name and execute the following command:

$ docker newtwork inspect ${NETWORK_NAME}

Where the network name is the name printed by docker-compose on the first line, in this case, the name would be smarthousetests_default, and note the IP address for graylog container, in our case named smarthousetests_graylog_1. The output for graylog will be similar to

            "eb410e97891a323aca5120bb46e319cad66b63e181ad60f2fdb6dbbe478dab01": {
                "Name": "smarthousetests_graylog_1",
                "EndpointID": "9204f94f1c3c7699c209a81e6b7053f55c70bf278be0c6d25e6139510feca712",
                "MacAddress": "02:42:ac:12:00:05",
                "IPv4Address": "172.18.0.5/16",
                "IPv6Address": ""
            }

When you’ll have the IP address of Graylog, set it to the file src/test/resources/patriot.properties to key io.patriot_framework.monitoring.addr, in this case

io.patriot_framework.monitoring.addr=172.18.0.5

By this step, all the preparation is done.

Executing the tests

When all of the preparation is done you can execute all of the tests by executing

$ mvn clean test

The command will:

  • Setup the testing environment and deploy testes applications

  • Execute the tests

  • Clean up all resources

Clean up after failed setup or tear down

If for some reason the setup phase ended in the middle of the setup (e.g. user interruption) or before tear down, you can delete all of the docker resources related to testing run by the following command

docker rm -f smarthome Internal1 External1 gateway
docker network rm SmartHome GatewayNetwork

Stopping the monitoring environment

If you’ll need to stop the monitoring environment, without deletion (e.g. stop the containers but with data intact) you can perform it by following command

$ docker-compose stop

Then the environment can be restarted by

$ docker-compose start

If you’d need to remove all of the resources (network, containers), then you’ll need to execute

$ docker-compose down

WARNING The removal of the containers does mean, that you’ll need to start the deployment from the top, because the network for the monitoring environment will be deleted and a new one might have different properties (e.g. network address).

Description of the SUT

This part will briefly describe the tests written and the way how capabilities of PatrIoT framework are used to achieve deployment of testbed as well as execution of tests. This example is based on existing solution of the smart home prototype built for demonstration purposes of Bulldog library. The existing solution was modified to accommodate for a purely virtual run using PatrIoT to simulate devices and allow the deployment of SUT into a virtual environment.

The implementation consists of two main parts:

  • Virtual smart home — the smart home prototype itself with controllable appliances

  • Smart home gateway — the gateway that is used as an interface to control the house

Virtual Smart Home

The core application which controls the appliances is the virtual smart home. It is written in Java and uses Apache Camel as the base technology to expose the functions of the smart home and build API which enables access to emulated end devices. All of the functions are opened to the world via RESTfull API. In the original solution, the Apache Camel was extended by SilverSpoon library, which wraps the access to the physical devices into an Apache Camel routes.

In order to move the scenario into the virtual world, all of the connections to the physical world were transformed by the usage of PatrIoT Data generator to the purely virtual objects, which are then used by the tests to simulate events similar to the real-world situations.

Smart Home Gateway

The Smart Home Gateway is an application that is connected to the virtual smart home via its API and does control the individual actions on the house as well as it responses to the events generated by it. It is written in Java as well and it is using Apache Camel to produce the API and connection layer to the house and Drools to build integration scenarios as Business processes. It enables more complex scenarios executed over the smart home, like composite actions which consist of multiple interactions with actuators — e.g. full lock of the house where all of the doors are locked, all lights are turned off and others.

Emulation of devices

In the original prototype, the devices were physically connected to the several buses and directly to the main computer of the smart house (Raspberry PI). Then all of them were used by a single Java application with direct access to them. All of the sensory data, as well as the actions performed on actuators were then collected and conducted by the application itself without any intermediate layer.

To simulate such behaviour a mockup strategy was used, so all of the physical devices were recreated in the virtual version of smart home via PatrIoT framework and are used directly within the smart house implementation.

The devices emulated from the original prototype by PatrIoT framework are:

  • Door actuator - state machine kind of actuator

  • Thermometer

  • Hygrometer

  • DHT11 device - combined thermometer and hygrometer

Door actuator

The door actuator is a device simulating the remotely controlled automatic doors. The device can be in two normal states and two transitional states:

  • Open - the doors are open to the world.

  • Closed - the doors are closed.

  • Opening - the doors are currently between states of Close and Open, which is set to take 1 second.

  • Closing - the doors are moving from Open to Close which takes 1 second.

Code defining the state machine of door actuator.
public class DoorActuator extends RotaryActuator {
    public DoorActuator(String label) {
        super(label, 1000);
        StateMachine machine = new StateMachine.Builder()
                .from("Closed")
                  .to("Open", "open", "Opening", 1000)
                .from("Open")
                  .to("Closed", "close", "Closing", 1000)
                .build();
        this.setStateMachine(machine);
    }
....
}

As seen in the above example, the code to define such behaviour is quite simple. The StateMachine.Builder object is used to define the states and transition function of such a device. The method from is used to denote the source state from which we’re making the transition. Use of method to then denotes, that upon delivery of event identified as open, the transition of states from Closed to Open should start and the immediate state of the StateMachine is set to be Opening. The same goes the other way around, that upon delivery of event close, the state will be switched to transitionary state Closing for 1 second and after the time is reached, the state Closed is set.

The change of state is executed by call of method controlSignal which delivers the set event to the underlying state machine and triggers the transition. Such call is encapsulated into two methods - openDoor and closeDoor, which adds check if the selected operation is permitted in the current state.

Close door method
    public void closeDoor() {
        String state = null;
        try {
            state = getStateMachine().getCurrent();
        } catch (Exception e) {
            System.out.println(e);
            return;
        }
        if (state.equals("Closed") || state.contains("Closing")) {
            return;
        }
        this.controlSignal("close");
    }

Hygrometer and thermometer

Both sensory devices hygrometer and thermometer are very similar in essence. Both behave as devices, which generates data from the specified range with set distribution and are polled for such readings by the controlling software. In our case the differences between hygrometer and thermometer are only the units, mean of expectation and standard deviation. To emulate such behaviour SimpleSensor object is used with a data generator defined by Normal Distribution.

The definition of Thermometer
public class Thermometer extends Sensor implements SimpleValueSensor<Float> {
    public static final String DEFAULT_UNIT = "°C";

    private String unit;
    private DataFeed dataFeed = new NormalDistVariateDataFeed(25, 1);
    private io.patriot_framework.generator.device.Device device = new io.patriot_framework.generator.device.impl.basicSensors.Thermometer(getLabel(), dataFeed);
...
}

As is seen in the example, the attribute device holds the object of Device kind, which is the basic type in the PatrIoT framework, then the NormalDistVariateDataFeed is used as the source of random data for measures of the temperature with mean of expectation being 25 degrees Celsius and a standard deviation of 1. Generated data are then obtained by a call of method getValue.

getValue method of Thermometer
@Override
    public Float getValue() {
        return device.requestData().get(0).get(Double.class).floatValue();
    }

The hygrometer is built in a very similar fashion, by use of the Device and NormalDistVariateDataFeed. Both of the devices are used as passive devices, which instead of producing and sending data are just polled by the controlling procedures, and processed later by the Apache Camel.

DHT11 Device

The DHT11 Device represents a very common sensor, that unlike sensors mentioned above measures and generates two values per request. It measures both temperature and humidity at the same time. This kind of sensor is very common in the outdoor weather stations. Such a device is prepared in the set of standard devices and contained in the device generator part of the PatrIoT framework. The device definition then looks like:

DHT11 passive sensor definition
public class DHT11Device extends Device {
    private DHT11 device;


    public DHT11Device(String label) {
        super(label);
        DataFeed hygroDF = new NormalDistVariateDataFeed(50, 30);
        DataFeed tempDF = new NormalDistVariateDataFeed(25, 3);
        device = new DHT11("dht11", tempDF, hygroDF);
    }

As seen above the example uses directly DHT11 kind of object which is imported from the PatrIoT framework, then the device is configured with two DataFeed objects to simulate both humidity and temperature. The methods to obtain data from such simulated device are very similar to the examples from Thermometer and Hygrometer.

Integration tests

The integration tests are written in PatrIoT with JUnit5 as the test execution engine and RESTassured library as a tool to easy develop and maintain the checks over the RESTfull API of both services in the SUT. Integration tests cover basic functions of the smart home and the gateway. The testing procedure can be split into the following phases:

  1. Setup the network simulation and deploy it to the docker.

  2. Deploy the applications into the simulated environment and configure its connection into the networks.

  3. Obtain the basic data from the simulated environment and configure the test execution engine.

  4. Conduct testing of SUT.

  5. Once the testing is done, tear down the simulated environment and clean up.

  6. Generate reports with the test results for further evaluation.

SUT setup and deployment

In order to achieve the deployment that would happen at right time before the test execution engine is invoked, the PatrIoTSetupExtension is used to register the steps necessary that must happen before the testing can be conducted. In the case of smart home tests, this extension is implemented by class EnvironmentSetup

The EnvironmentSetup class is registered to the PatrIoT framework by specific configuration in META-INF resource, and is responsible to start the deployment into the docker and at the and of testing clean up the simulation.

Content of src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
io.patriot_framework.samples.smarthome.utils.EnvironmentSetup

The file shown above registers the extension into the JUnit context. The base abstract class PatriotSetupExtension is then connected to the JUnit’s lifecycle manager and responses to the specific events within the engine. In this case it executes method setUp when the JUnit is fully loaded but before the test execution. The EnvironmentSetup then creates an object of DockerDeployment kind, contains the actual definition of the SUT and of the simulated environment.

The definition of the deployment starts with the network setup. This is done by the method DockerDeployment::createTopology. The method creates virtual networks within the docker context and deploys router component into it to set the visibility and routes between them.

The network topology definition in DockerDeployment::createTopology
        Topology top = new TopologyBuilder(3)
                .withCreator("Docker")
                .withRouters()
                .withName("Internal1")
                .createRouter()
                .withName("External1")
                .createRouter()
                .addRouters()
                .withNetwork("SmartHome")
                .withIP(smartHomeNetwork)
                .withMask(24)
                .create()
                .withNetwork("GatewayNetwork")
                .withIP(gatewayNetworkIP)
                .withMask(24)
                .create()
                .withNetwork("BorderNetwork")
                .withInternet(true)
                .create()
                .withRoutes()
                .withSourceNetwork("SmartHome")
                .withDestNetwork("GatewayNetwork")
                .withCost(1)
                .viaRouter("Internal1")
                .addRoute()
                .withSourceNetwork("GatewayNetwork")
                .withDestNetwork("BorderNetwork")
                .withCost(1)
                .viaRouter("External1")
                .addRoute()
                .withSourceNetwork("SmartHome")
                .withDestNetwork("BorderNetwork")
                .withCost(4)
                .addRoute()
                .buildRoutes()
                .build();

The code above creates two networks called SmartHome and GatewayNetwork, it also defines BorderNetwork which represents connection to the real network with access to the internet. The networks are then interconnected by two routers. The router Internal1 is responsible for communication between SmartHome and GatewayNetwork, and External1 is responsible to connect GatewayNetwork to the BorderNetwork. As a result of the code above the topology, object representation is created.

Actual deployment of the topology in DockerDeployment::createTopology
        DockerController c = new DockerController();
        getHub().getManager().setControllers(Arrays.asList(c));
        getHub().deployTopology(top);

The code above is then responsible to register the DockerController to the shared context of the PatrIoT via the PatriotHub singleton and requests the topology to be deployed into the docker. The getHub method is used as a safe wrapper around the PatriotHub::getSingleton() method.

With the topology created and connected the applications can be started. In our case, the deployment of applications is done by method DockerDeployment::deployApplications. As explained above our goal is to deploy two applications virtual smart home and smart gateway.

Creation of Application objects.
    void deployApplications() {
        Application smartHome = new Application("smarthome", "Docker");
        Application gateway = new Application("gateway", "Docker");

As is seen in the code, the deployment is started by the creation of Application objects. These objects are the main interface to interact with containers deployed into the simulated environment. Once the applications are created, the deployment can start.

Deployment of virtual smart home into the virtual environment and extraction of containers ip address
        getHub().deployApplication(smartHome, "SmartHome", "smarthome");

        String ip = smartHome.getAddressForNetwork("SmartHome");

Since the smart gateway needs to know where the virtual smart home is deployed, it is essential to ensure that the virtual smart home is deployed first and its ip address is known. That is done by call of deployApplication method from PatriotHub. The method is invoked with three arguments:

  • smartHome object of the virtual smart home application

  • "SmartHome" the name of a primary network where should be the application deployed.

  • "smarthome" tag of the docker image which will be deployed as such application.

After the deployment is finished, the container is created and the application starts. This is the time when the IP address of the docker container can be obtained. To achieve it, the Application object is used, which contains all of the identifiers to get the IP from the docker daemon.

Deployment of the smart gateway with the configuration of a virtual smart home IP address
        getHub().deployApplication(gateway,
                "GatewayNetwork",
                "patriotframework/smart-gateway",
                Arrays.asList(
                        "IOT_HOST=" + ip + ":8282",
                        "IOT_WS_HOST=" + ip + ":9292"
                ));

        LOGGER.info("Sleeping for 30 to wait for deployments to finish.");
        try {
            Thread.sleep(30000);
        } catch (InterruptedException ex) {
            LOGGER.warn("Error in while sleeping", ex);
        }
    }

As the last step of the deployment, the smart gateway can be deployed. In this instance the method deployApplicaton is invoked with four instead of three arguments:

  • gateway object of the smart gateway application.

  • "GatewayNetwork" the primary network where the smart gateway should be connected.

  • "patriotframework/smart-gateway" the container image tag to be used deployed.

  • List of environment variables defined as strings. Here we are declaring how the smart gateway should connect to the virtual smart home.

Once these steps are performed, the main execution thread is paused for 30 seconds to ensure that the applications are started before the tests can proceed.

The basic test scenarios for virtual smart home

At this phase the of the process the testing can begin and the test classes are created. The most simple test that would be executed is defined in the FirePlaceTest class. This class is extending the abstract base class SmartHomeBase which holds the common configurations for the virtual smart home-related tests. The base class contains the BeforeAll method named init which is executed prior to the start of any test within the test class.

init method of SmartHomeBase abstract class
    @BeforeAll
    public static void init() {
        LOGGER.debug("Instantiation of test base class");
        String address = null;
        try {
            address = PatriotHub.getInstance().getApplication("smarthome").getAddressForNetwork("SmartHome");
        } catch (PropertiesNotLoadedException e) {
            LOGGER.error("Couldn't load properties file", e);
        }
        RestAssured.baseURI = "http://" + address;
        RestAssured.port = 8282;
        RestAssured.defaultParser = Parser.JSON;
    }

The code above is responsible for the configuration of shared context used by RestAssured library. In this case, it obtains the IP address of the virtual smart home and configures it as a base URI for the RestAssuret to test against. As can be seen the PatriotHub is used again to get the Application object of the virtual smart home and use it for IP extraction.

The fireplace test itself is then quite simple. Our goal is to test that once we request the virtual smart home to turn on the fireplace it will respond to us with the correct message that the fireplace was turned on.

The example test of fireplace
    @Test
    public void testTheSwitchingFireplace() {
        given()
                .basePath("/fireplace/on")
                .when()
                .get()
                .then()
                .statusCode(200)
                .and()
                .body("enabled", equalTo(true))
                .body("label", equalTo("fireplace"));
    }

Since the base URI was configured in the RestAssured static context, it is only needed to configure which endpoint of the API will be invoked. The HTTP method GET is used with the path /fireplace/on. The test then expects, that the status code of response will be equal to 200 HTTP.OK and the body of the response will contain JSON object with two attributes {"enabled": true, "label":"fireplace"}. In case any of those assumptions is not met, the test will be marked as a failure.

The next test that is implemented is a test of the reset endpoint. This endpoint of the smart home is responsible to set all of the actuators to the default state:

  • Windows and doors are closed.

  • Fireplace is turned off.

  • All of the led diodes are turned off.

  • AC turned off.

The first steps of the test are to change the state of several devices out of the default state. In this case it opens a window and turns fireplace on. Then it verifies that the actions were performed and the virtual smart home is in the desired state.

The setup steps of the reset test
        given()
                .basePath("/window/open")
                .when()
                .get()
                .then()
                .statusCode(200);

        waitOneSec();

        given()
                .basePath("/window")
                .when()
                .get()
                .then()
                .statusCode(200)
                .and()
                .body("state", equalTo("Open"));

        given()
                .basePath("/fireplace/on")
                .when()
                .get()
                .then()
                .statusCode(200);

Once the state is verified, the /reset endpoint can be requested and then the verification of state can proceed. For the verification of the state, the /all endpoint of the virtual smart home is used. This endpoint contains all of the sensors and actuators and their state or measured value. In this particular case, it is expected that the window will be in state Closing and the fireplace will be off.

The request to reset endpoint and verification of the state afterwards
given()
                .basePath("/reset")
                .when()
                .get()
                .then()
                .statusCode(200);

        given()
                .basePath("/all")
                .when()
                .get()
                .then()
                .statusCode(200)
                .and()
                .body("[0].label", equalTo("rear-door"))
                .body("[0].state", startsWith("Closing"))
                .body("[1].label", equalTo("fireplace"))
                .body("[1].enabled", equalTo(false))
                .body("[12].label", equalTo("air-conditioner"))
                .body("[12].enabled", equalTo(false));

        waitOneSec();

        given()
                .basePath("/window")
                .when()
                .get()
                .then()
                .statusCode(200)
                .and()
                .body("state", equalTo("Closed"));

The last test implemented in this example is test of cooperation between the virtual smart home and smart gateway. The test simulates one of the endpoints of smart gateways used by mobile app. The scenario is simple, to open the front door by requesting the gateway. In this particular case, the endpoint /mobile of smart gateway with query parameter button=7 must be delivered to the smart gateway and then the virtual smart home must instructed to open the front door.

Hence in this scenario two separate Applications will be needed, the smart gateway to order the command and virtual smart home to check if the action was performed.

The test of the smart gateway doorOpenTest
    private static RequestSpecification getRequest(String appName, String networkName, int port) {
        try {
            return given()
                    .baseUri("http://" +
                             PatriotHub.getInstance()
                                       .getApplication(appName)
                                       .getAddressForNetwork(networkName))
                    .port(port);
        } catch (Exception ex) {
            return null;
        }
    }
    @Test
    public void doorOpenTest() {
        RestAssured.defaultParser = Parser.JSON;
        RequestSpecification home = getRequest("smarthome", "SmartHome", 8282);
        RequestSpecification gateway = getRequest("gateway", "GatewayNetwork", 8283);

        gateway.basePath("/mobile")
                 .param("button", "7")
               .when()
                 .get()
               .then()
                 .statusCode(200);
        home.basePath("/door")
            .when()
              .get()
            .then()
                .statusCode(200)
              .and()
                .body("label", equalTo("front-door"))
                .body("state", startsWith("Opening"));
    }

In case above the place where the helper function getRequest is used to communicate with PatriotHub and obtain ip address of selected application and encapsulates this IP address into the form of RequestSpecification which is internal object of REST assured. Since now we have object ready to be used by REST assured we can straight away perform the tests. As explained above, as first we request the the mobile endpoint to open the door, where we expect response with status code 200 HTTP.OK and then using the home RequestSpecification we check if the action was performed.