Cloud & Engineering

Devendra Khanolkar

Jumpstarting Camel Blueprint Testing

Posted by Devendra Khanolkar on 09 March 2016

Testing, tech, blueprint, fuse, camel

Blueprint is a dependency injection framework specification for OSGi, standardized by the OSGi Alliance group. Apache Aries is one of the several implementations of this spec.

There are a couple ways you can get Camel to work with an OSGi framework -

  • Apache Aries Blueprint
  • Spring Dynamic Modules (aka Spring DM)

Spring DM has been deprecated, so the only recommended way (and the best practice) of building your favorite Camel route on an OSGi powered container like Karaf is to use Blueprint (which under the hood is Apache Aries).

Developing an OSGi project could be ‘potentially’ time consuming due to its steep learning curve. By the time you’ve built your OSGi project, got it running on your shiny new OSGi container, you’re pretty much out of time (and of breath), so route testing, is more of an after thought. Mind you, the documentation around Blueprint testing isn’t the greatest.

So, here’s a jump-start guide to get you up and going in virtually no time.

Camel offers several API’s to cover route testing but since we are referring to the Blueprint DI framework, I’ll stick to Camel’s CamelBlueprintTestSupport class, which offers heaps of powerful features that should tick all your Blueprint testing needs.

Getting ready

Start by extending your RouteTest class from CamelBlueprintTestSupport.

TIP: These examples are based on Groovy but dont let them scare you away. The Java equivalent should be pretty trivial.

If you use an IDE, it’ll help you nut out the basic methods you need to ‘override’.

First up is the getBlueprintDescriptor method. It loads your context.xml plus your routes (comma seperated).

@Override
protected String getBlueprintDescriptor() {
	[
		'/OSGI-INF/blueprint/config.xml',
		'/OSGI-INF/blueprint/camel-context.xml',
		'/OSGI-INF/blueprint/say-hello-route.xml'
	].join ','
}

TIP - CamelBlueprintTestSupport only lets you test 1 Camel Context (try testing 2 and you’ll figure the first found is used for testing.)

Override the useOverridePropertiesWithConfigAdmin method and return the name of your property-placeholder “persistent-id”.

@Override
protected String useOverridePropertiesWithConfigAdmin(Dictionary props throws Exception {
	/* 
	 treat Dictionary like a Map
	*/
	props.with {
	 put 'foo', 'bar'
	}
	/* 
	 return the "persistent-id"
	 if your cfg file is foo-bar.cfg, then return foo-bar
	*/
    return "foo-bar"
}

TIP - If you fancy keeping your test properties in a file rather than injecting them into the props - Dictionary, then simply override the loadConfigAdminConfigurationFile method to tell Camel which file to load

@Override
protected String[] loadConfigAdminConfigurationFile() {
    /* 
     String[0] = tell Camel the path of the .cfg file to use for OSGi ConfigAdmin in the blueprint XML file
     String[1] = tell Camel the persistent-id of the cm:property-placeholder in the blueprint XML file 
    */
    /* 
     An OSGi backed container like Karaf has a convention to 
     load all *.cfg files from the /etc folder
    */
    return new String[]{ 'src/test/resources/etc/foo-bar.cfg', 'foo-bar' }
}

To tie any OSGi services (eg. a JMS or JDBC connector) during start up override the addServicesOnStartup method just like this -

@Override
protected void addServicesOnStartup(Map<String, KeyValueHolder<Object, Dictionary>> services) {
	services.put 'javax.jms.ConnectionFactory', asService( new ActiveMQConnectionFactory(), 'jmsServiceKey, 'jmsService' )
}

Mock’g goodness -

Say, you’ve got an extenal HTTP endpoint (a REST url) to invoke in your route but just need to mock it out when testing. Start by Overriding the isMockEndpointsAndSkip method -

@Override
public String isMockEndpointsAndSkip() {
	return 'https4:.*|https:.*|amq:queue:.*'
}

TIP - Notice the '|’ delimiter? It is to mock 2 or more endpoints in the same route. What this really does is that it’ll skip invoking those endpoints when your route executes.

Here’s a good one to mock. Say you need to test a route with a SAP endpoint. There are no ‘in-memory’ SAP servers which you could spin up for route testing. In these cases, you would want to strip out that bit in the route which invokes the ‘real’ SAP RFC with a mock’d SAP RFC. Here’s how you can achieve that. You start by mocking the SAP endpoint first -

@Override
public String isMockEndpointsAndSkip() {
	return "sap:.*"
}

Then turn on the adviceWith (more on this later) feature by overriding a method -

@Override
boolean isUseAdviceWith() {
	return true
}

And finally use the getMockEndpoint method to mock out the SAP endpoint -

def mockSAPEndpoint = getMockEndpoint( 'mock:sap:destination:nplDest:NAME_OF_THE_SAP_RFC' )
	// Ensure the endpoint was indeed invoked
	mockSAPEndpoint.setExpectedMessageCount(1)
	mockSAPEndpoint.setSynchronous(true)
        mockSAPEndpoint.returnReplyBody(new Expression() {
		@Override
		def evaluate(Exchange exchange, Class type) {
			// return a mock'd response back
			return 'foo bar'
		}
	})

GOTCHA - The test logs typically indicate what endpoints have been mocked. Here is what I’d see if I were to mock a https endpoint - https://platform.deloitte.com.au.s3-website-ap-southeast-2.amazonaws.com

Adviced endpoint [https4://platform.deloitte.com.au.s3-website-ap-southeast-2.amazonaws.com] with mock endpoint [mock:https4:platform.deloitte.com.au.s3-website-ap-southeast-2.amazonaws.com]

TIP - Remember to invoke the assertIsSatisfied() method on the Deloitte Platform EngineeringMockEndpoint object, else the mock endpoint won’t get intercepted and would never return the “Foo Bar” string payload back to the route.

Advicing your routes

Now say your route has parts which just cannot be tested during unit testing. For such scenarios, CamelBlueprintTestSupport has a very powerful feature called ‘adviceWith’. adviceWith sees a lot of usage when you need to replace a part of the route with your own logic during testing.

Say you dont wish to capture the mock endpoints “inboundPayload” and just need to send a response back (without sending the message to the actual endpoint)?

context.getRouteDefinition ( 'fooBarRoute' ).adviceWith(context, new AdviceWithRouteBuilder() {
		void configure() throws Exception {
			// Here we mock the salesforce query endpoint
			interceptSendToEndpoint( 'salesforce:query*' ) 
				.skipSendToOriginalEndpoint()
				.setBody('Foo Bar')
		}
	})

A clever use of ‘adviceWith’ is to replace a bean’s real method with a test method. In the Camel world its referred to as ‘weaving’ (In or Out). There are quite a few weaveBy* method at your disposal. Here’s a sample -

context.getRouteDefinition("fooBarRoute").adviceWith(context, new AdviceWithRouteBuilder() {
	@Override
	public void configure() throws Exception {
		// bean id
		weaveByToString('.*innovation.*') 
		.selectFirst() // choose which occurrence you wish to replace 
		.replace() // this is where you weave that bean out
		.bean( au.com.Deloitte Platform Engineering.Deloitte Platform EngineeringAtWork.class , 'liberateThenInnovate' )
	}
})

TIP - While weaving beans out from your route, you can easily run into method ambiguity issues. So to avoid such pitfalls, clearly specify the method name from your TestOnlyBean class. Here we replace the first occurrence of a bean with id innovation and call the liberateThenInnovate() method from the Deloitte Platform EngineeringAtWork class

Another candidate for using adviceWith is to intercept an endpoint and detour the exchange. Here we intercept the Salesforce query and detour the exchange to a log -

context.getRouteDefinition('fooRoute').adviceWith context, new AdviceWithRouteBuilder() {
		void configure() throws Exception {
			interceptSendToEndpoint('salesforce:query*')
				.skipSendToOriginalEndpoint()
				.to( 'log:salesforceLog')
		}
	}

TIP - Some may find the ‘adviceWith’ feature a tad flaky. This can be due to advicing a route more than once or advicing a route that has already been started.

Conclusion

I’ve barely scratched the surface here. However, it should be enough to get you started on your bold new journey of Blueprint route testing.

 

If you like what you read, join our team as we seek to solve wicked problems within Complex Programs, Process Engineering, Integration, Cloud Platforms, DevOps & more!

 

Have a look at our opening positions in Deloitte. You can search and see which ones we have in Cloud & Engineering.

 

Have more enquiries? Reach out to our Talent Team directly and they will be able to support you best.

Leave a comment on this blog: