Fork me on GitHub

This document provides information for Unit Testing and Integration Testing of your services using Rio. Rio provides classes to assist in the setup and execution of services for either testing approach. Rio itself has test cases that use the integration testing framework built on JUnit. We will review this framework in this section.

Rio provides integration testing, allowing the integration testing of your services together; with or without the the instantiation of Rio infrastructure services. This section will discuss both options.

Unit Testing

Unit testing services using Rio can be done using one of many testing frameworks. The examples provided with Rio use JUnit and the Failsafe Maven plugin. The goal going into this is to unit test your service without having to instantiate the Rio infrastructure services (Provision Monitor, Cybernode, Lookup).

The example below illustrates how an opstring composed of multiple services that use associations can be tested. In this scenario, the Calculator service has the Addition, Subtraction, Multiply and Divide services injected into it. All of these services are created and injected as they would be in an actual Rio deployment, but in our scenario they are bootstrapped locally.

This occurs because Rio provides an extension for a local LookupCache to be used, one that is wired into the lifecycle events of a service being instantiated by a Cybernode.

/**
 * Example testing the Calculator service and it's required services from the
 * OperationalString
 */
@RunWith(Parameterized.class)
public class ITCalculatorTest extends ITAbstractCalculatorTest {
    String opstring;
    Calculator calculator;

    @Parameterized.Parameters
    public static Collection<Object[]> data() {
        String opstring = System.getProperty("opstring");
        Assert.assertNotNull("no opstring given", opstring);
        return Arrays.asList(new Object[][] {{ opstring }});
    }

    public ITCalculatorTest(String opstring) {
        this.opstring = opstring;
    }

    @Before
    public void setupCalculator() throws Exception {
        StaticCybernode cybernode = new StaticCybernode();
        Map<String, Object> map = cybernode.activate(new File(opstring));
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String beanName = entry.getKey();
            Object beanImpl = entry.getValue();
            if (beanName.equals("Calculator"))
                calculator = (Calculator) beanImpl;
        }
    }

    @Test
    public void testBean() throws RemoteException {
        testService(calculator);
    }

}

In the setupCalculator() method, the org.rioproject.cybernode.StaticCybernode is created and passed the opstring provided as a property when configuring the Failsafe plugin.

The StaticCybernode instantiates the service(s) declared in the opstring, and returns a java.util.Map composed of key value pairs mapping to the ServiceBean names, and each respective service implementation.

In order to used the returned service implementation, you must cast it accordingly.

StaticCybernode.activate() can instantiate all the services in your opstring or just specific services. If you want to create specific services, pass in the name (as found in the opstring) for the service(s) you want.

The corresponding opstring for the Calculator example is shown below. Note the association declarations.

import org.rioproject.config.Constants

deployment(name:'Calculator') {
    groups System.getProperty(Constants.GROUPS_PROPERTY_NAME,
                              System.getProperty('user.name'))

    artifact id:'service', 'org.rioproject.examples:calculator:1.0'
    artifact id:'service-dl', 'org.rioproject.examples:calculator:dl:1.0'

    service(name: 'Calculator') {
        interfaces {
            classes 'org.rioproject.examples.calculator.Calculator'
            artifact ref:'service-dl'
        }
        implementation(class:'org.rioproject.examples.calculator.service.CalculatorImpl') {
            artifact ref:'service'
        }
        associations {
            ['Add', 'Subtract', 'Multiply', 'Divide'].each { s ->
                association name:"$s", type:'requires', property:"${s.toLowerCase()}"
            }
        }
        maintain 1
    }

    ['Add', 'Subtract', 'Multiply', 'Divide'].each { s ->
        service(name: s) {
            interfaces {
                classes "org.rioproject.examples.calculator.$s"
                artifact ref:'service-dl'
            }
            implementation(class: "org.rioproject.examples.calculator.service.${s}Impl") {
                artifact ref:'service'
            }
            maintain 1
        }
    }
}

It is important to remember that the StaticCybernode utility requires that all necessary classes (and resources) are in the classpath of the JVM in order to activate the service bean(s). Although the opstring(s) may have resources/artifacts defined, all classes must be in the classpath of the JVM.

Integration Testing Using Rio Infrastructure Services

The Rio test infrastructure provides a way to bootstrap core Rio infrastructure services (Provision Monitor, Cybernode, Lookup Service, Webster) as part of your integration test case. The test infrastructure is built on JUnit 4.5+, and is what Rio itself uses as part of it's supplied project tests.

When writing a JUnit test case, your test case code needs to simply use the following annotation:

@RunWith(RioTestRunner.class)
public class MyTest {
...
}    

The org.rioproject.test.RioTestRunnner class is a custom extension of org.junit.runners.BlockJUnit4ClassRunner that provides the functionality of the Rio Test Framework to standard JUnit 4.5+ tests.

The RioTestRunnner also provides support for the following annotations:

@SetTestRunner
Defines the property to invoke to set the TestManager. The TestManager will be injected by the RioTestRunner prior to test case method invocation.

public class MyTest {
    @SetTestManager
    static TestManager testManager;

    @BeforeClass
    public static setup() {
        Assert.assertNotNull(testManager);
        testManager.startReggie();
    }

    @Test
    public void testIt() {
        ...
    }

    ...
}

The @SetTestManager annotation can be applied at either the method or field level.

@IfPropertySet
Annotation to indicate that a test is to be run if a specific property is set.

@IfPropertySet(name="run.long.test", value="true")
@Test
public void testSomethingThatTakesALongTime() {
    ...
}

The @IfPropertySet annotation can be applied at either the class or method level.

Configuring The Test Manager

The Rio Test Manager is a Groovy class that requires a configuration file in order to initialize and run. By default the configuration file is loaded as a resource using reasonable defaults. The default configuration that ships with Rio is included below:

manager {

    String rioHome = System.getProperty("RIO_HOME")
    StringBuilder classPath = new StringBuilder()
    ["rio-start.jar", "resolver-api.jar", "start.jar", "groovy-all.jar"].each { jar ->
        if(classPath.length()>0)
            classPath.append(File.pathSeparator)
        classPath.append(rioHome+'/lib/'+jar)
    }

    classPath.append(File.pathSeparator).append(System.getProperty("JAVA_HOME")).append("/lib/tools.jar")
    execClassPath = classPath.toString()

    inheritOptions = true

    /* Get the directory that the logging FileHandler will create the service log.  */
    String logExt = System.getProperty(Constants.GROUPS_PROPERTY_NAME, System.getProperty('user.name'))
    String opSys = System.getProperty('os.name')
    String rootLogDir = opSys.startsWith("Windows")?'${java.io.tmpdir}':'/tmp'
    String name = System.getProperty('user.name')

    log = "${rootLogDir}/${name}/logs/${logExt}/"

    jvmOptions = '''
    -javaagent:${RIO_HOME}${/}lib${/}rio-start.jar
    -Djava.protocol.handler.pkgs=org.rioproject.url
    -XX:+HeapDumpOnOutOfMemoryError -XX:+UseConcMarkSweepGC -XX:+AggressiveOpts -XX:HeapDumpPath=${RIO_HOME}${/}logs
    -server -Xms8m -Xmx256m -Djava.security.policy=${RIO_HOME}${/}policy${/}rio.policy
    -DRIO_HOME=${RIO_HOME} -DRIO_TEST_ATTACH
    -Dorg.rioproject.groups=${org.rioproject.groups}
    -Dorg.rioproject.service=${service}'''

    /*
    * Remove any previously created service log files
    */
    cleanLogs = true

    mainClass = 'org.rioproject.start.ServiceStarter'

    monitorStarter = '${RIO_HOME}/config/start-monitor.groovy'

    cybernodeStarter = '${RIO_HOME}/config/start-cybernode.groovy'
}

The configuration file can be overridden by setting the following system property:

-Dorg.riproject.test.manager.config=path-to-config-file

The properties of the TestManager configuration file are documented below:

Property Description
execClassPath The classpath to use to start the core Rio services
inheritOptions If set, and set to true, the options (input arguments) to the JVM will be inherited from the enclosing process that has created the TestManager. If this property is set to true, input arguments of the enclosing process will be used to exec service JVMs created by the Rio TestManager, and will also override any settings declared in the jvmOptions property below.
jvmOptions Options and arguments to use when starting the JVM. For more details on JVM options, check here
log Where to create a log for the service being started. The service token will be replaced by the name of the starter file. For start-monitor the service name will be monitor, etc ..
cleanLogs Remove any previously created service log files
mainClass The classpath to use to start the core Rio services
monitorStarter The configuration file used to start the Provision Monitor.
cybernodeStarter The configuration file used to start a Cybernode.
config Optional configuration file that the RioTestRunner will use to confgure underlying utilities.
harvesterOpString The opsting the harvest utility will use. The harvest utility is deployed to all CYbernodes and is used to collect (harvest) configured artifacts. You can read more about harvesting here.
harvestDir The directory to save harvested artifacts.

Test Configuration

The Rio testing infrastructure is set up to use a Groovy configuration file to run and configure your test classes. The following snippet illustrates the elements involved:

ITCalculatorDeployTest {
    groups = "Calculator"
    /* Comma separated locator names */
    //locators = "10.0.1.4:9160, 10.0.1.6:32189"
    //testManager = new org.rioproject.test.TestManager(true)
    numCybernodes = 1
    numMonitors = 1
    opstring = 'src/main/conf/calculator.groovy'
    autoDeploy = true
    //harvest = true
    //timeout
}

The enclosing element of the configuration must be the class name of the test class sans the package name. This allows the Rio testing infrastructure to locate the configuration for the test class.

Property Description
groups Lookup discovery group(s) to use when creating discovery management for the test case.
locators Lookup locator(s) to use when creating discovery management for the test case.
testManager Optionally decare the TestManager to use. If not declared a new org.rioproject.test.TestManager(true) is created.
numCybernodes Optional number of Cybernodes the test infrastructure should create
numMonitors Optional number of Provision Monitors the test infrastructure should create.
opstring The OperationalString associated with the test case.
autoDeploy Whether to automatically deploy the associated opstring (above) when the test case is started.
harvest Whether to harvest artifacts at the end of the test classes execution (run with all after class statements).
timeout The default timeout (per test method and in millis) to use for test case execution.
If JUnit's @Test(timeout="xxx") is used, JUnit's declaration takes precedence.

Back to top

Version: 5.0-M3. Last Published: 2013-06-04.