Unit Testing Java Callouts with TestNG, JMockit and Easytesting

If you are writing Java Callouts in Apigee Edge then you should unit test them as well! But how do you do that with a Java Callout that will be deployed to Apigee? This article will describe how to accomplish this with TestNG, JMockit and Easytesting.

I used Intellij Idea (2016.2) and there is a sample project located in the apigee-javacallout-testng repository.

When you write a Java Callout your class will depend on two jar files, message-flow-1.0.0.jar and expressions-1.0.0.jar and your class must implement the Execution interface. This interface includes a method with the following signature:

public ExecutionResult execute (MessageContext msgCtxt, ExecutionContext exeCtxt)

So you have to mock the MessageContext and ExecutionContext classes to execute your Java Callout locally.

1) Clone the Github repository.

git clone git@github.com:swilliams11/apigee-javacallout-testng.git

2) Install the message-flow-1.0.0.jar and expressions-1.0.0.jar onto your local machine. Unfortunately, these jar files are not stored in the Maven central repository, so you have to install them manually to your local maven repository. Execute the following command in your terminal window.

cd apigee-javacallout-testng

mvn install:install-file \
 -Dfile=lib/expressions-1.0.0.jar \
 -DgroupId=com.apigee.edge \
 -DartifactId=expressions \
 -Dversion=1.0.0 \
 -Dpackaging=jar \
 -DgeneratePom=true

mvn install:install-file \
 -Dfile=lib/message-flow-1.0.0.jar \
 -DgroupId=com.apigee.edge \
 -DartifactId=message-flow \
 -Dversion=1.0.0 \
 -Dpackaging=jar \
 -DgeneratePom=true

3) Open the project located in the apigee-javacallout-testng/callout directory in Intellij Idea and then open the JavaCalloutTest.java file to view the seven unit tests.

3756-screen-shot-2016-10-17-at-23531-pm.png

Pom File

The POM file has declared all the necessary dependencies and will execute all the packages listed in the testng.xml file. See the POM file in the Github repository.

TestNG XML file

The testng.xml file will execute all the tests in the com.apigee.callout directory.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="testng">
  <test name="testng" preserve-order="true">
    <packages>
      <package name="com.apigee.callout" />
    </packages>
  </test>
</suite>


JavaCalloutTest

This file contains all the tests and mocks. It is not an exhaustive list but it will demonstrate how to setup an Apigee Java Callout and create tests that you can execute locally.

The first thing that you will notice in this file is that I declared MessageContext and Execution context as instance variables.

MessageContext msgCtxt;
ExecutionContext exeCtxt;

The setup() method is annotated with @BeforeMethod so this method is executed before every test method, which means that we get a new MessageContext and ExecutionContext for every test case.

MessageContext is an interface; therefore, we have to mock the methods that we use in the JavaCallout class so that JavaCallout class will function correctly while it is tested.

The ExecutionContext is an actual class, so we don't have to mock out any methods, but we do need a mock instance of it, so that is why I have new Mockup<ExecutionContext>() {}.getInstance();

@BeforeMethod
public void setUp() throws Exception {
    //Mock the MessageContext
    msgCtxt = new MockUp<MessageContext>() {
        private Map variables;
        public void $init() {
            variables = new HashMap();
        }

        @Mock()
        public <T> T getVariable(final String name){
            if (variables == null) {
                variables = new HashMap();
            }
            return (T) variables.get(name);
        }

        @Mock()
        public boolean setVariable(final String name, final Object value) {
            if (variables == null) {
                variables = new HashMap();
            }
            variables.put(name, value);
            return true;
        }

        @Mock()
        public boolean removeVariable(final String name) {
            if (variables == null) {
                variables = new HashMap();
            }
            if (variables.containsKey(name)) {
                variables.remove(name);
            }
            return true;
        }

    }.getMockInstance();

    //Mock the ExecutionContext
    exeCtxt = new MockUp<ExecutionContext>(){}.getMockInstance();
}


Testing Private Methods

Let's look at the first test case where I am testing a private method. I know there are different reasons for testing private methods versus testing a class through its public interface; I will leave that for you to decide. If you want to your test private methods, then keep reading otherwise you can skip ahead to the next section. If your private methods have complicated logic or you are calling a private method which calls another private method, etc., then it may be easier to test the private methods as well as the public methods.

I used Easytesting to accomplish this. First I had to create a new properties map, which doesn't include any properties and then I created a new JavaCallout instance with the properties map passed to its constructor. This is required even if you don't have any properties. Note that when this callout executes in Edge, it will instantiate the class and pass all the properties included in the Java Callout. Invoke the private method as shown below and then assert the result.

There are several other test methods that test the private functions in the Java Callout and you can review those at your leisure.

/*
Test the private method varName().
It should always add a prefix to the property passed to it.
 */
@Test
public void testAddPrefix() throws Exception {
    Map<String, String> properties = new HashMap<>();

    // GIVEN
    String testVariable = "myvar";
    JavaCallout callout = new JavaCallout(properties);

    // WHEN
    String result = method("addPrefix")
            .withReturnType(String.class)
            .withParameterTypes(String.class)
            .in(callout)
            .invoke(testVariable);
    // THEN
    Assert.assertEquals(result, "prefix_myvar");
}

Testing Public Methods

Let's take a look at testing the execute method. In this test I included several properties, which are the same properties that would be included in the Java Callout policy in the <Properties> element.

I have to set the request.content variable on the MessageContext so that when the JavaCallout class executes on my local machine, it will find the request payload in that variable. Note, that when this policy executes in Edge, it will automatically populate the request.content flow variable.

The callout.execute() calls the method under test.

The assertions check that the Java Callout executed successfully and it checks that several flow variables were populated.

/*
This method tests that that the Java Callout executes successfully.
and finds the supplied regex within the response.content variable.
 */
@Test
public void testExecute_Payload_Pattern1() throws Exception {
    //GIVEN
    Map<String, String> properties = new HashMap<>();
    properties.put("toMatch", "request.content");
    properties.put("username", "user@email.com");
    properties.put("checkHeaders", "false");
    properties.put("regex", pattern1);

    //WHEN
    msgCtxt.setVariable("request.content", "<script>this is a test</script>");

    JavaCallout callout = new JavaCallout(properties);
    ExecutionResult result = callout.execute(msgCtxt, exeCtxt);

    //THEN
    Assert.assertEquals(result, ExecutionResult.SUCCESS);
    Assert.assertEquals(msgCtxt.getVariable("flw.apigee.status"), "success");

    Assert.assertEquals(msgCtxt.getVariable("flw.apigee.patternFound"), pattern1);
    Assert.assertEquals(msgCtxt.getVariable("flw.apigee.match"), "<script>this is a test</script>");
    }


Using the Java Callout

The Java Callout policy is shown below. The properties map in the test methods represents the <Properties> element in the Java Callout policy (see below).

<JavaCallout name='Java.Regex'>
  <Properties>
    <!-- JS injection patterns -->
    <Property name="regex">(?i)(<\s*script\b[^>]*>[^<]+<\s*.+\s*[s][c][r][i][p][t]\s*>)</Property>
    <Property name="matchHeaders">true</Property>
    <Property name="toMatch">request.content</Property>
    <property name="username">mysample@email.com</Property>


  </Properties>
  <ClassName>com.apigee.callout.JavaCallout</ClassName>
  <ResourceURL>java://regex-callout.jar</ResourceURL>
</JavaCallout>



Test Headers

The last test method that I will cover mocks the request headers; however, the same approach can be used to mock query parameters as well.

Again, I included properties that must be included for this specific test. Now notice the headers HashMap, which includes headers that will be included in the MessageContext. Note, that if you are executing this in Apigee Edge, then it will populate the MessageContext with headers by default. Since we are running this locally, we need to mock the headers that we want to test. The setMsgCtxtHeaders() is a helper method to populate the headers in the MessageContext as Edge would.

/*
This method tests that that the Java Callout
finds the supplied regex within a request header.
*/
@Test
public void testExecute_Headers() throws Exception {
    //GIVEN
    Map<String, String> properties = new HashMap<>();
    properties.put("toMatch", "request.content");
    properties.put("checkHeaders", "true");
    properties.put("username", "user@email.com");
    properties.put("regex", pattern1);
    msgCtxt.setVariable("request.content", "this is a test");

    Map<String, String> headers = new HashMap<>();
    headers.put("x-username", "myusername");
    headers.put("x-hacking", "<script>console.log(password)</script>");
    setMsgCtxtHeaders(headers);

    //WHEN
    JavaCallout callout = new JavaCallout(properties);
    ExecutionResult result = callout.execute(msgCtxt, exeCtxt);

    //THEN
    Assert.assertEquals(result, ExecutionResult.SUCCESS);
    Assert.assertEquals(msgCtxt.getVariable("flw.apigee.status"), "success");

    Assert.assertEquals(msgCtxt.getVariable("flw.apigee.patternFound"), pattern1);
    Assert.assertEquals(msgCtxt.getVariable("flw.apigee.match"), "<script>console.log(password)</script>");
}


Please view the apigee-javacallout-testng repository for more details and see the associated README.md located in the callout directory.


How do I Compile the jar, Execute Tests and Deploy the Sample Proxy to Apigee Edge?

In the apigee-javacallout-testng directory there is a POM file configured to compile the Java Callout, execute the TestNG tests and deploy the proxy to the org specified in the command line. Execute the following command. Be sure to change:

1) the username to your Apigee org admin username

2) the password to your Apigee org password and

3) change the org to your Apigee org name

mvn install -Ptest -Dusername=orgadmin@email.com -Dpassword=orgadminpwd -Dorg=orgname

After you execute the above command you should see the following output.

[INFO] ------------------------------------------------------------------------
[INFO] Building java-callout-test 1.0
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:copy-resources (default) @ java-callout-test ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 9 resources
[INFO]
[INFO] --- apigee-edge-maven-plugin:1.1.0:configure (configure-bundle) @ java-callout-test ---
[INFO] No config.json found. Skipping package configuration.
[INFO]
...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] RegexCallout ....................................... SUCCESS [  3.293 s]
[INFO] java-callout-test .................................. SUCCESS [  0.823 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4.238 s
[INFO] Finished at: 2016-10-17T20:21:06-05:00
[INFO] Final Memory: 16M/437M
[INFO] ------------------------------------------------------------------------
...
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] RegexCallout
[INFO] java-callout-test
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building RegexCallout 1.0.0
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-resources-plugin:3.0.0:copy-resources (copy-files-on-build) @ regex-callout ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] ignoreDelta true
[INFO] Copying 1 resource
[INFO] Copying file regex-callout.jar
[INFO]
[INFO] --- maven-resources-plugin:3.0.0:resources (default-resources) @ regex-callout ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory ...
[INFO]
[INFO] --- maven-compiler-plugin:2.3.2:compile (default-compile) @ regex-callout ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-resources-plugin:3.0.0:testResources (default-testResources) @ regex-callout ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory ...
[INFO]
[INFO] --- maven-compiler-plugin:2.3.2:testCompile (default-testCompile) @ regex-callout ---
[INFO] Nothing to compile - all classes are up to date
[INFO]
[INFO] --- maven-surefire-plugin:2.19.1:test (default-test) @ regex-callout ---

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running TestSuite
objc[34658]: Class JavaLaunchHelper is implemented in both ...
Tests run: 7, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.079 sec - in TestSuite

Results :

Tests run: 7, Failures: 0, Errors: 0, Skipped: 0

[INFO]
Deployed revision is: 7
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] RegexCallout ....................................... SUCCESS [  3.223 s]
[INFO] java-callout-test .................................. SUCCESS [ 10.747 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 14.070 s
[INFO] Finished at: 2016-10-17T20:21:29-05:00
[INFO] Final Memory: 22M/365M
[INFO] ------------------------------------------------------------------------
Comments
davissean
Staff

very cool, thanks posting for this example @swilliams

Version history
Last update:
‎10-17-2016 01:37 PM
Updated by: