Adventures in debugging: Camel mock endpoints are real
I made an interesting discovery the other week, which I want to share with you!
I was trying to set up Pact contract testing for a Camel on Spring Boot application, and kept running into issues with a mock endpoint.
The endpoint in question was camel-mongodb3 (now renamed camel-mongodb) component. Here’s what the original Camel route looked like - edited for the purposes of this blog post!:
from("direct:get-orgs").id("get-organisations-route")
.to("mongodb3:myMongoClient?database=&collection=&operation=findAll")
.process(new ResponseMessageProcessor())
.log("Returning body - ${body}");
I wanted to mock the mongodb3:
endpoint in a unit test, so that it didn’t attempt to connect to a real MongoDB instance. So I use mockEndpointsAndSkip()
in the test class:
mockEndpointsAndSkip("mongodb3:*");
When I ran the test, I saw the confirmation that the component had been mocked in the logs:
Adviced endpoint [mongodb3://myMongoClient] with mock endpoint [mock:mongodb3:myMongoClient]
But – despite mocking the Mongo component – I could see that it was still attempting to make a real connection to a Mongo instance on localhost, port 27018, and failing:
java.lang.IllegalStateException: Failed to load ApplicationContext …
20:26:48.838 [cluster-ClusterId{value=’5d55b1f8717ae36ad8ceeddd’, description=’null’}-localhost:27018] INFO org.mongodb.driver.cluster - Exception in monitor thread while connecting to server localhost:27018 com.mongodb.MongoSocketOpenException: Exception opening socket
Why was the Mongo component still being instantiated even though I had mocked it?
I spent a long time trying to figure out what was going on. I may have banged my head against the keyboard a few times.
Was the dependency for Pact testing playing havoc with Camel? Was it causing the mock endpoints to be ignored?
I turned to the trusty tool I use when I encounter situations like this: isolation.
To me, isolation goes something like this:
-
Comment out blocks of code.
-
Comment out some more code.
-
Strip everything out of the application, until you’re left with almost nothing.
-
Hope you’ve found the problem.
But that still didn’t give me an answer.
So I replaced the Mongo component with another fairly innocuous component: the JMS component.
That did work: the JMS component seemed to get mocked and skipped successfully.
So why was it that one component seemed to be mocked and skipped, but another was not?
I started debugging the application in IntelliJ (love the support for decompiling and breakpoints!) and stepping through the code, line-by-line.
(This is how I get when I refuse to be stumped by a problem. I don’t call it a day and find a workaround. No, I get a bit, erm, obsessed.)
I stepped through the code, right down to the deepest parts of the Camel source.
And I made this discovery:
Original endpoints are still initialised, even if they have been mocked.
Here’s the proof. When stepping through the code, I noticed that this line in MongoDbEndpoint.java gets hit when the Camel Context starts up in my unit test:
This is a line which creates the Producer for an endpoint. In other words, even though you might mark an endpoint as mocked and skipped, the component is still there. It is still initialised. It will just be skipped when a message flows through the route.
This is important, as it means that components which need some initialisation, like the Mongo component, will still initialise themselves in your unit test.
And the same goes for the JMS component. Mocking it with:
mockEndpointsAndSkip("jms:*");
…means it will still get initialised with a Producer. I didn’t notice this before, because the JMS component doesn’t initialise a connection when a Producer is created:
So you’ve got to take this fact into account, if you need to mock a component.
. . .
What did I learn? Lots:
-
The createProducer() method is always called for endpoints, even when they have been mocked.
-
If there is code in createProducer() which tries to create a connection to some third-party system, this code will still be executed, even when the component is mocked.
-
For mongodb3, there is a line in MongoDbEndpoint#createProducer which calls initializeConnection – and this attempts to connect to MongoDb to check the database/collection exists, given that there is a MongoClient bean in the context.
-
Also, Camel Producers are created in the order that they appear in routes; so if the mongodb component appears before others, it will fail first and cause an exception which terminates the program.
-
So, for MOST components, mocking should work without issue. It’s just that mongodb is one exception to the rule, because it tries to initialise a connection to a database when the endpoint is created.
-
Sheer stubbornness on problem-solving wins again.
And diving into the source code is a great way of understanding more about the tools I use every day.
My solution going forward is either to use a MockBean to create a fake MongoClient object; or, I’ll replace the endpoint URIs in the Camel route with property placeholders, e.g.:
.to("{{some.uri}}")
…then, I can set the endpoint URI explicitly to mock:....
in my unit test configuration, avoiding the Mongo component altogether.
Learning every damn day. :-)