Saturday, August 15, 2009

Testing OSGi Bundles with Pax Exam, Groovy and Gradle

You can't be an expert on everything. There are a few topics in life I've given up on, forever to be ignorant:

  • Botany - Don't ask me the name of any plant or tree. I won't know.
  • Hardware Specs - Telling me the model number and speed on your new laptop just makes me confused. I stopped reading the Computer Shopper years ago.
  • Maven - I'm happy to use it IF SOMEONE ELSE CREATES THE POM. Life's too short for that much XML.
So what was I to do with Pax Exam? This framework allows you to write JUnit or TestNG tests and then executes them in the OSGi container of your choosing. It's a cool framework but much of the documentation assumes a willingness to scroll through lines and lines of Maven XML. Ugh. Life's too short to grapple with build systems.

So the nut is cracked. I've got Groovy unit tests running across Equinox, Felix, and Knopflerfish from both Gradle and IntelliJ IDEA. No Maven in sight.

Writing Pax Exam Tests in Groovy

Once it was all configured, writing Pax Exam tests were simple. Start with a plain old JUnit 4 test and do two things:
  1. Annotate it to run with the Pax Exam JUnit runner
  2. Configure it to provision the Groovy-all JAR
Here it is, complete with imports and ready for a little copy-paste action.
import org.junit.runner.RunWith
import org.ops4j.pax.exam.junit.JUnit4TestRunner
import org.junit.Test
import org.ops4j.pax.exam.junit.Configuration
import org.ops4j.pax.exam.Option
import static org.ops4j.pax.exam.CoreOptions.*

@RunWith (JUnit4TestRunner)
class GroovyIntegrationTest {

@Configuration
public Option[] configure() {
[
provision(
mavenBundle().groupId('org.codehaus.groovy').artifactId('groovy-all').version('1.6.4')
)
] as Option[]
}

@Test
public void testFramework() {
println 'Hello from Pax-Groovy!'
}
}

A few things to notice... CoreOptions was statically imported, so the methods provision() and mavenBundle() are resolved. The configure() method returns an Array of Option objects, so the "as Option[]" cast is needed. And by default, the tests are going to run on Felix 1.8 (or thereabouts).

Before Pax Exam becomes useful you're going to want to provision your bundle as the system under test, get access to the BundleContext it was loaded with, and specify more containers to run on.

Provisioning your bundle and running on multiple containers is a matter of configuring the test differently:
@Configuration
public Option[] configure() {
[equinox(),
felix(),
knopflerfish(),
provision(
bundle(new File('./../out/production/Filter4osgi.jar').toURI().toString()),
mavenBundle().groupId('org.codehaus.groovy').artifactId('groovy-all').version('1.6.4')
)] as Option[]
}
Here I'm specifying Equinox, Felix, and Knopflerfish. There's also options for allEquinoxVersions, allFelixVersions, and allKnopflerfishVersions, as well as allFrameworks and allFrameworksVersions to test on the world. That options takes a few minutes to execute! Provisioning my own bundle can be done by loading it as a file URL. This example isn't very robust but it proves the concept.

The BundleContext is available as an injectable bean from Pax Exam. If you want the BundleContext in your unit test, to perhaps test that services were registered correctly, then add this as a field:
@Inject
BundleContext bundleContext
And here is a simple test to make sure that my filter4osgi library was correctly installed:
@Test
public void test_BundleIsLoaded() {

def found = bundleContext.bundles.find {
'filter4osgi' == it.symbolicName
}
Assert.assertNotNull('filter4osgi bundle not loaded!', found)
}
Running Pax Exam Tests from Gradle

The Gradle build was slick. Tell Gradle to load your bundle as a file URL and the rest is just boilerplate dependency management:
usePlugin 'groovy'

repositories {
mavenCentral()
flatDir dirs: [
'./../out/production', // filter4osgi jar built earlier
]
}

dependencies {
groovy group: 'org.codehaus.groovy', name: 'groovy-all', version: '1.6.4'
compile(
'org.ops4j.pax.exam:pax-exam:1.0.0',
'org.ops4j.pax.exam:pax-exam-container-default:1.0.0',
'org.ops4j.pax.exam:pax-exam-junit:1.0.0',
'junit:junit:4.5',
'org.osgi:org.osgi.core:4.0.1',
':filter4osgi:',
)
}
"gradle test" is the only target you'll ever need.

Running Pax Exam Tests from IntelliJ IDEA

Your unit tests will show up once for each container you're targeting. So my two tests run against 3 containers shows up as 6 tests in the test runner window, plus a sweet ASCII art logo:

One nice side effect of Maven is that IDEA can generate files off it, so you don't need to worry about the transative dependencies in the IDE, it's all configured for you. When using Gradle you do need to set up the IDE manually. I added the following JARs as dependencies and it all worked fine (with one exception):
commons-discovery-0.4.jar
commons-logging-1.1.jar
easymock-2.4.jar
junit-4.4.jar
log4j-1.2.12.jar
ops4j-base-lang-1.0.0.jar
ops4j-base-monitors-1.0.0.jar
ops4j-base-net-1.0.0.jar
org.osgi.core-4.0.1.jar
osgi-3.4.0.jar
pax-exam-1.1.0-SNAPSHOT.jar
pax-exam-container-default-1.1.0-SNAPSHOT.jar
pax-exam-container-rbc-1.1.0-SNAPSHOT.jar
pax-exam-container-rbc-client-1.1.0-SNAPSHOT.jar
pax-exam-junit-1.1.0-SNAPSHOT.jar
pax-exam-junit-extender-1.1.0-SNAPSHOT.jar
pax-exam-junit-extender-impl-1.1.0-SNAPSHOT.jar
pax-exam-runtime-1.1.0-SNAPSHOT.jar
pax-exam-spi-1.1.0-SNAPSHOT.jar
pax-exam-testng-1.1.0-SNAPSHOT.jar
pax-exam-tutorial-1.1.0-SNAPSHOT.jar
pax-runner-no-jcl-1.1.0.jar
You can surely discard a lot of these JARs. But I added the list of completeness. It's just the IDE setup. I did have one issue with running the tests in the IDEA. Between runs I needed to manually delete my $TEMP_DIR/paxexam_runner_[user] folder. It sounds like no one else is experiencing this issue and I'm using a snapshot I built myself from source. The mailing list has been pretty responsive but it's a mystery error for now.

That's the end of it, folks. I'm going to the back yard to sit in the kiddie pool with my daughter. It's hot as a mutha in my office.

3 comments:

Chris Brind said...

Got as far as "No Maven in sight" - ah, excellent. I'll save this to read in more detail tomorrow!! Thanks in advance.

Sigmund said...

Getting this when karaf starts:

java.lang.NoClassDefFoundError: groovy/lang/GroovyObject

Ideas?

Hamlet D'Arcy said...

@Sigmund sounds like the Groovy bundle is not loaded in the container.