$ docker pull patriotframework/patriot-router:latest
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
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:
Change the directory to the selected component.
Run mvn clean package
to produce a compiled java program.
Build the docker image by running docker build . --tag myapp/${COMPONENT}
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.
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
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
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).
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
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.
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.
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
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.
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.
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");
}
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.
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
.
@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.
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:
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.
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:
Setup the network simulation and deploy it to the docker.
Deploy the applications into the simulated environment and configure its connection into the networks.
Obtain the basic data from the simulated environment and configure the test execution engine.
Conduct testing of SUT.
Once the testing is done, tear down the simulated environment and clean up.
Generate reports with the test results for further evaluation.
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.
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.
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.
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
.
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.
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.
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.
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.
@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.
@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.
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.
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.
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.